Skip to content

fix(issue-fix): serialize continuous issue repair - #2006

Open
xielixing wants to merge 21 commits into
GCWing:mainfrom
xielixing:codex/bitfun-loopx-issue-fix-repair
Open

fix(issue-fix): serialize continuous issue repair#2006
xielixing wants to merge 21 commits into
GCWing:mainfrom
xielixing:codex/bitfun-loopx-issue-fix-repair

Conversation

@xielixing

Copy link
Copy Markdown

Summary

  • add the LoopX-backed Issue Fix surface from issue enumeration through single-issue planning/execution
  • add a serial autonomous portfolio: one active issue, deferred successors, typed write/publish approval, and one issue per heartbeat turn
  • report actual host-run state (queued, running, ok, error, cancelled) instead of treating an enabled schedule as active work
  • normalize Windows extended-length paths before passing worktree paths to Git or UI contracts

Fixes: N/A

Type and Areas

Type: Feature, regression fix, UI/UX, test, docs

Areas: Rust services integration, desktop/Tauri, web UI, worktree handling, LoopX integration

Motivation / Impact

The first integration could schedule a generic LoopX heartbeat that spent many turns reconciling control-plane state without selecting and repairing a concrete issue. It also projected an enabled heartbeat as if code repair were currently running. This change gives Issue Fix its own bounded execution contract, keeps multi-issue work serial and isolated, and preserves the authority gate before branch publication.

Verification

  • cargo test -p bitfun-services-integrations --features loopx-issue-fix autonomous::tests - 14 passed
  • cargo test -p bitfun-services-integrations --features loopx-issue-fix absolute_references_are_rejected --lib - 1 passed
  • cargo test -p bitfun-services-integrations --features git normalized_path_strips_extended_windows_prefix_for_git_cli --lib - 1 passed
  • cargo test -p bitfun-core path_string_strips_extended_windows_prefix_for_ui_contracts --lib - 1 passed
  • cargo check -p bitfun-desktop --lib - passed
  • cargo test -p bitfun-desktop continuous_tests --lib - 3 passed
  • focused Issue Fix Vitest suite - 8 passed
  • pnpm --dir src/web-ui run type-check - passed
  • pnpm run prepare:mobile-web - passed
  • pnpm run desktop:dev - visible BitFun desktop window launched from this branch; web dev server listening on port 1422

Reviewer Notes

  • The existing single-issue probe/plan/execute APIs remain available.
  • Autonomous start can repair an existing queue without adding duplicate issues.
  • Legacy natural-language approval gates are migrated to the typed Issue Fix decision scope; approval is still required before write/publish authority is configured.
  • A cancelled previous turn is displayed as cancelled while the recurring schedule remains enabled; it is no longer shown as actively fixing.
  • No issue branch, push, or PR is performed until the typed authority gate is approved.

Checklist

  • This PR is focused and does not include secrets, temporary prompts, generated scratch files, or unrelated artifacts.
  • Relevant verification is recorded above, or skipped checks are explained.
  • User-facing strings, docs, and locales are updated where applicable.

xlx1212 and others added 11 commits July 31, 2026 16:06
Adds a probe-gated bridge to the external `loopx` CLI's issue-fix capability,
which supplies a deterministic decision skeleton (which route to take for an
issue, how a PR's lifecycle projects forward) while performing no writes of its
own. BitFun keeps every side effect and supplies every piece of evidence.

Behind a non-default `loopx-issue-fix` feature, deliberately outside
`product-full` until the chain is verified against a real repository.

Two behaviors were found by testing against the real CLI rather than assumed:

- LoopX reports domain refusals as `{"ok": false, "error": ...}` on stdout *and*
  exits nonzero. Parsing stdout before checking the exit status keeps the
  structured reason instead of reporting a bare exit code.
- `--validation-label` is required for the `fix_pr` route. Without a named
  validation surface LoopX downgrades to `triage_only` even when the context is
  grounded, the issue reproduces, and the scope is bounded.

Sets `PYTHONUTF8=1` on every invocation: LoopX's 123 subprocess call sites pass
`text=True` without `encoding=`, so on a non-UTF-8 locale it decodes `gh` output
as the local codepage and dies. The env var fixes all of them at once and needs
no patch to LoopX.

Contract tests drive the real CLI and skip cleanly when it is absent, matching
the runtime probe gate.

Co-Authored-By: Claude <noreply@anthropic.com>
Adds `list_issues`, closing the one real backend gap for automatic issue
fixing: the service could fetch a single issue by id but had no way to
discover which issues exist. Five `list_pull_requests` implementations
already existed; issues had no equivalent.

Returns a new lightweight `ReviewPlatformIssueSummary` rather than the
existing `ReviewPlatformIssueEvidence`, which carries a full body and every
comment — enumerating a hundred issues must not pull all of that.

Provider differences handled:

- GitHub returns pull requests inline from its issues endpoint, marked only
  by a `pull_request` member, so they are filtered out. Continuation is
  inferred from a full page because `gh` surfaces no Link headers here; the
  check runs before PR filtering, since a page of only PRs can still be
  followed by issues.
- GitLab addresses issues by project-scoped `iid`, not the global `id`, and
  has no "all" state literal — the filter is omitted entirely instead of
  sent empty. Continuation comes from its `x-next-page` header.

Takes a request struct because the sibling `issue` method already sits at
clippy's argument limit.

Covered by mocked-HTTP tests for the GitLab path and an ignored test that
drives the real `gh` CLI for GitHub, which mocks cannot reach.

Co-Authored-By: Claude <noreply@anthropic.com>
Adds `RepositoryContextBuilder`, the evidence half of the integration. LoopX
holds no code-reading ability and refuses to guess, so the quality of its route
decisions depends entirely on what BitFun reports here.

Every constraint LoopX's validator enforces is enforced at construction time
instead, because a rejected payload costs a whole subprocess round trip:
source-id shape and uniqueness, reference length, summary length measured after
whitespace collapsing, the 16-source cap, advisory-only trust for memory and
expert sources, and a pinned revision whenever a source claims current
freshness. References must be repository-relative — an absolute local path would
leak the operator's filesystem layout into a payload that can reach a public
issue thread.

`context_status` and `ungrounded_required_aspects` mirror LoopX's grounding rules
locally, so a caller can decide what else to read without paying for a
subprocess call. A contract test compares the prediction against the real CLI
aspect by aspect, which is what would catch the two drifting apart.

That comparison corrected a mistaken assumption of mine. I had assumed a partial
context caps the route at `triage_only`; it does not. Two contract tests now pin
the real behavior: a grounded context without `--validation-label` yields
`triage_only`, while a merely partial context *with* the label yields `fix_pr`.
LoopX distinguishes "which test files did you read" from "how will you check this
fix", and only the latter gates opening a PR. Comments and the design doc that
stated otherwise are corrected.

Moves the module into a directory to hold the new submodule.

Co-Authored-By: Claude <noreply@anthropic.com>
Runs one issue through feasibility → branch → PR lifecycle behind typed
outcomes. The value is in the typing: LoopX's decisive fields sit at
non-obvious paths, and reading them wrong is the failure mode that turns a
refusal into an approval.

Two paths were corrected by testing against the real CLI rather than assumed:
`state` lives under `observation`, and `state_bucket` under
`grouped_monitor_projection` — neither is top level, though the markdown
rendering shows them flattened. A unit test asserting the wrong shape would
have looked fine, so the mocked packets now match verified reality.

Unrecognized routes and lifecycle decisions are errors, never defaults.
Silently mapping an unknown value onto something permissive could open a pull
request LoopX had declined. Optional evidence still degrades to empty, since
guessing there is harmless.

`plan_issue` skips branch preparation entirely on a non-fix route. Under
`ExecutionMode::Execute` that would otherwise create a branch LoopX just
refused to justify, so the skip is a safety property.

`may_open_pull_request` requires the fix route, a ready review packet, and
passing validation together. The feature ships with no runtime kill switch, so
this gate lives on the action itself rather than relying on a disabled toggle.

Co-Authored-By: Claude <noreply@anthropic.com>
Adds `review_platform_list_issues` across the three layers the existing
`review_platform_get_issue` already spans: core facade wrapper, Tauri command,
and typed frontend binding.

The command takes an owned DTO because `ReviewPlatformListIssuesRequest`
borrows its strings and cannot be deserialized directly.

Co-Authored-By: Claude <noreply@anthropic.com>
Opens from a chat-header button into a right-panel tab: issue list on the left,
selected issue's detail on the right. Follows the pull-requests button and
`createReviewPlatformTab` for how the tab opens.

Row state lives in `issueFixRunState` as pure functions, so the mapping from
LoopX's decisions onto what a user sees is testable without rendering. The
mapping that matters: a `user_gate` renders as blocked, never as done, and
`nextIssueToRun` returns null while any row is blocked. Advancing past a gate
would defeat the gate — that is the one behavior LoopX raises it for.

Reason codes are shown verbatim rather than paraphrased, so a declined fix
explains itself in LoopX's own vocabulary.

The panel resolves `owner/repo`, host, and platform from the workspace's
selected remote, since the header only knows the local checkout path. Platform
is threaded through rather than hardcoded, so GitLab works too.

The error status key is `stopped`, not `failed`: the i18n audit tracks
`statuses.failed` as a shared term with a governance budget, and adding a 43rd
duplicate would have needed that budget raised. Renaming was the honest fix
rather than moving the baseline.

Co-Authored-By: Claude <noreply@anthropic.com>
Multi-issue runs need continuation, budgets, and human gates. BitFun already owns
all three in `thread_goal`, so this bridge adds none of its own — it only
translates. That is also why nothing here reaches for a scheduler or quota:
LoopX's issue-fix capability contributes neither, and its 35 modules import
neither.

The mapping that carries weight is `user_gate` → `Blocked`. `plan_serial_run`
additionally returns no next issue while a gate is open, since handing one back
would invite a caller to step over the gate rather than resolve it. `Blocked`
stays resumable, so answering the question picks the run back up instead of
stranding it.

`is_resumable` duplicates the agent-runtime predicate rather than pulling in that
crate for four lines. The test enumerates every `ThreadGoalStatus` variant, so a
new one has to be classified deliberately instead of silently defaulting to
non-resumable.

Note on clippy: adding the runtime-ports dependency brings one pre-existing
warning from that crate into this feature's build. It is not new code of mine.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Closes the gap my earlier summary glossed over: the panel could list and select
issues, but nothing connected it to the orchestrator, so no fix action was
reachable. A Start button now walks the selected issues serially through
`feasibility`, driving the row states that were already implemented and tested.

Planning only, verified rather than asserted: `ExecutionMode::Execute` has zero
production callers, and the Tauri surface mentions execute exactly once — in the
comment saying it has none. Nothing reachable from the UI can create a branch,
run a command, or open a pull request.

The run loop tracks state in a local variable rather than reading React state
back each iteration, which would lag a render behind and could re-run an issue.
It stops as soon as `nextIssueToRun` returns null, so an open gate halts the run
instead of being stepped over.

No repository context is generated yet, because nothing in BitFun generates one.
LoopX therefore reports `not_provided` and declines to open a pull request. That
is the honest current state, and its reason codes name exactly which evidence is
missing — better than asserting a validation surface nobody checked.

`loopx-issue-fix` is enabled for the desktop crate only, leaving `product-full`
untouched so release builds of other consumers are unaffected.

Co-Authored-By: Claude <noreply@anthropic.com>
…tion spawning

- Add loopx-issue-fix to the services-integrations product-full feature
  group after real-repo validation against GCWing/BitFun GCWing#1849
- Register the feature and its optional dependency owners in the core
  boundary rules so the product-full assembly stays explicit
- Wrap caller-declared validation commands with cmd /c on Windows:
  LoopX spawns them with subprocess.run(argv) and no shell, so .cmd shims
  such as pnpm fail with WinError 2
- Fix a pre-existing lib-test compile gap in plugin_source tests
- Record the real-repo verification and the Windows defect in the
  integration design doc
The start button used to only project LoopX routes; a fix_pr route had no
execution path, so no model was ever called. Wire the missing half:

- New issue_fix_execute Tauri command: feasibility gate, then submit the
  fix task as a dialog turn to the session's agent loop (same scheduling
  path as a manual message, so the model's streaming output appears in
  the chat transcript)
- Empty agent_type lets the coordinator resolve the session's own mode
  instead of overriding it
- Panel: plan first, then submit fix_pr issues to the agent; non-fix
  routes record their reason codes and move on
- Declare remote-workspace policies for all issue_fix commands and the
  pre-existing review_platform_list_issues gap
- Document the execution model in the integration design doc
@xielixing

Copy link
Copy Markdown
Author

Follow-up repair for the Issue-Fix gate/cancellation lifecycle:

  • User decisions now validate one typed Issue-Fix gate from the todo list and return a compact receipt. The submit path no longer performs quota plus full status projections before and after the write.
  • cancel disables the matching Cron job and requests cancellation of its active agent turn. reject leaves the continuous loop enabled.
  • The panel closes the answered gate immediately from the receipt, shows localized processing/stopped state, and refreshes the full LoopX projection in the background.
  • Merged current origin/main; conflict resolution preserves both appearance-market and loopx-issue-fix feature ownership.

Validation:

  • cargo test -p bitfun-services-integrations --features loopx-issue-fix autonomous::tests -- --nocapture (15 passed)
  • cargo test -p bitfun-desktop only_cancel_stops_the_continuous_host_loop --lib -- --nocapture (1 passed)
  • pnpm --dir src/web-ui exec vitest run ...IssueFixUserQuestion.test.tsx ...issueFixRunState.test.ts (10 passed)
  • pnpm --dir src/web-ui run gen:types (43 export tests passed)
  • pnpm type-check:web (passed)
  • pnpm i18n:contract:test:ci (37 tests, 23 passed / 14 intentionally skipped)
  • pnpm check:core-boundaries and pnpm check:core-boundaries:test (67 passed)
  • git diff --check and public/private path scan passed; only the existing compile_market_source_with_app_data_dir dead-code warning remains.

…host loop

Replace the thread-goal bridge with a Kernel-owned autonomous loop: selected
issues become LoopX intake todos, and BitFun's persistent cron service wakes
one agent session every 10 minutes with an English host preamble plus the
LoopX `heartbeat-prompt --compact` contract (thin mode depends on skill
packs absent from BitFun sessions).

- Project user gates from `todo list` instead of the quota preview, which
  is compacted to two entries; issue-linked gates win, unlinked gates still
  surface so an open gate can never stall the loop invisibly.
- Add issue_fix_stop_autonomous (kill switch that also sweeps orphaned or
  duplicate jobs and survives a broken registry) and a quota-free
  issue_fix_autonomous_poll for the panel's 30s poll loop, since
  `quota should-run` appends a rollout event per call.
- Serialize start/stop/answer-wake on HOST_LOOP_LOCK, self-heal duplicate
  cron jobs, refresh the heartbeat prompt snapshot at every gate answer,
  and surface host-loop failures (lastError/consecutiveFailures) in the UI.
- Guard every panel state write with a monotonic ticket, pause polling
  during mutations, add a Stop button, re-project after failed gate
  answers, and collapse scheduled heartbeat turns into an expandable chip.
- Prune ~60 dead issue-fix locale keys; keep en-US/zh-CN/zh-TW in parity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@xielixing
xielixing force-pushed the codex/bitfun-loopx-issue-fix-repair branch from bc5dc05 to 276583f Compare August 4, 2026 11:53
xlx1212 and others added 9 commits August 4, 2026 20:14
# Conflicts:
#	scripts/core-boundaries/rules/feature-rules.mjs
#	src/apps/desktop/Cargo.toml
#	src/crates/services/services-integrations/Cargo.toml
#	src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx
#	src/web-ui/src/flow_chat/components/modern/FlowChatHeader.tsx
#	src/web-ui/src/flow_chat/components/modern/UserMessageItem.tsx
#	src/web-ui/src/flow_chat/tool-cards/AskUserQuestionCard.tsx
During the first overnight full-batch run the agent hit cargo lock
contention in a worktree, ran Stop-Process -Force against "stale cargo
processes", and terminated the BitFun app hosting it (twice). The host
preamble now carries a hard rule: never force-kill processes not started
in the current turn; on lock contention wait, use a worktree-local
CARGO_TARGET_DIR, or record a blocker todo.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GitHub's /repos/{o}/{r}/issues endpoint interleaves pull requests with
issues, and the client-side PR filter ran after taking one page — so once
the continuous Issue-Fix loop opened 19 PRs, the first page starved down
to a handful of issues and the panel showed a truncated queue with a zero
count. /search/issues with is:issue filters server-side, reports an exact
total_count, and keeps newest-first ordering explicitly.

Verified by the live github_issue_page_enumerates_a_public_repository
test (network + authenticated gh).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…block

The heartbeat agent surfaces review/merge requests and authority gates as
LoopX user todos, but the panel only rendered the single active gate card
— merge-PR actions were visible only inside chat NOTIFY summaries. The
panel now lists every open user_gate/user_action todo in a read-only
"pending your action" block with provider jump links. No mutation surface
is offered: gates answer through the existing question card, review/merge
actions resolve on the provider side and close via the Kernel's monitors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The heartbeat agent wrote user-lane todo text free-form, so the pending
block ranged from terse ("Authorize merge of PR GCWing#2039") to a full drafted
comment pasted inline. The host preamble now prescribes one compact line:
action + PR/issue ref + which issue it serves + the state justifying it,
with long material routed to --note/--evidence. Display side: the primary
URL moves to the jump icon instead of repeating inline, and rows clamp to
two lines.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Gates and review requests were only visible inside the Issue-Fix panel
(and chat NOTIFY text), so users working elsewhere missed them. The panel
now raises app-level notification cards through the existing notification
system when a new gate or user_action todo appears: warning card for
gates, info cards for actions (digested into one card when a beat
produces more than three), each with an "Open panel" action. The first
projection seeds silently so pre-existing items do not re-toast on mount.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The continuous Issue-Fix host must work against any repository, but the
hardening rules named BitFun's own toolchain (node/vite, pnpm, cargo,
CARGO_TARGET_DIR). Restate them generically — never kill processes not
started this turn, keep build outputs inside the worktree, remove the
worktree and its caches at terminal closeout, one sibling worktrees
folder per repository — and route repository-specific policy where LoopX
says it belongs: the goal's active state and registry, read and written
by the agent, never baked into the automation prompt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three afternoon PRs ballooned to ~8.8k added lines because the agent
branched its fix worktrees from the host checkout's HEAD, which sat on
the in-flight feature branch; the whole feature diff rode along into
each PR. The preamble now requires basing every fix branch on
origin/<default> after a fetch, never on the host checkout's current
branch. The three contaminated branches were rebased onto origin/main
and force-pushed (PRs GCWing#2058/GCWing#2059/GCWing#2060 now carry only their own fix).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
It described the abandoned plan/execute + thread-goal design; the
implemented architecture (heartbeat host loop, kernel-owned state,
panel projection) diverged from it entirely.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant